#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <assert.h>
#include "game.h"

const float FORBIDDEN_TURNS[8] = {
    12.0f, 14.0f,
    15.0f, 14.0f,
    12.0f, 26.0f,
    15.0f, 26.0f
};

static inline int sign(int v) {
    if (v == 0) return 0;
    return v > 0 ? 1 : -1;
}

static void animation(Ghost* ghost, Game* game);
static void point_in_direction(Ghost* ghost, int direction);
static void calc_next_direction(Ghost* ghost, Game* game);
static int calc_distance(Ghost* ghost, Game* game, int x, int y, int index);

static int get_opposite_direction(int dir) {
    switch (dir) {
        case DIR_RIGHT: return DIR_LEFT;
        case DIR_LEFT: return DIR_RIGHT;
        case DIR_UP: return DIR_DOWN;
        case DIR_DOWN: return DIR_UP;
    }

    return -1;
}

static void get_direction_vector(int dir, int* x, int* y) {
    switch (dir) {
        case DIR_RIGHT:
            *x = 1;
            *y = 0;
            break;

        case DIR_LEFT:
            *x = -1;
            *y = 0;
            break;

        case DIR_UP:
            *x = 0;
            *y = -1;
            break;
        
        case DIR_DOWN:
            *x = 0;
            *y = 1;
            break;
    }
}

void ghost_init(Ghost* ghost, Game* game, int direction, GhostAI ai) {
    memset(ghost, 0, sizeof(Ghost));

    ghost->x = 14*8;
    ghost->y = 14*8 + 4;

    ghost->dx = 1;
    ghost->dy = 0;
    ghost->direction = direction;

    ghost->next_cx = obj_cell_x(ghost);
    ghost->next_cy = obj_cell_y(ghost);

    ghost->next_dir = DIR_RIGHT;
    ghost->subpixel = 0.0f;
    ghost->speed = 0.75f;
    ghost->frame = 0;
    ghost->fright = FALSE;
    ghost->eaten = FALSE;
    ghost->reverse = FALSE;
    ghost->in_house = TRUE;
    ghost->is_leaving_house = FALSE;
    ghost->is_entering_house = FALSE;
    ghost->dot_counter = 0;
    ghost->scatter = TRUE;
    ghost->ai = ai;

    point_in_direction(ghost, direction);
    animation(ghost, game);
}

static void move(Ghost* ghost, float speed) {
    ghost->subpixel += speed;
    ghost->x += ghost->dx * (int)ghost->subpixel;
    ghost->y += ghost->dy * (int)ghost->subpixel;
    ghost->subpixel = fmodf(ghost->subpixel, 1.0f);
}

static int is_dot_tracker_reached(Ghost* ghost, Game* game) {
    if (game->use_ghost_dot_count) {
        assert(ghost->ai.is_dot_tracker_reached != NULL);
        if (ghost->ai.is_dot_tracker_reached(game->level_index, ghost->dot_counter))
            return TRUE;
    } else {
        assert(ghost->ai.is_global_dot_tracker_reached != NULL);
        if (ghost->ai.is_global_dot_tracker_reached(game->level_index, game->ghost_dot_count)) {
            return TRUE;
        }
    }

    return FALSE;
}

void ghost_leave_house(Ghost* ghost) {
    if (!ghost->in_house) return;
    ghost->is_leaving_house = TRUE;
    ghost->in_house = FALSE;
}

static void house_update(Ghost* ghost, Game* game) {
    ghost->eaten = FALSE;

    // check if dot counted was reached
    if (is_dot_tracker_reached(ghost, game)) {
        ghost_leave_house(ghost);
    }

    if (ghost->is_leaving_house) {
        // first, move toward center of the house
        int house_cx = 14 * 8;

        if (ghost->x != house_cx) {
            ghost->direction = ghost->x < house_cx ? DIR_RIGHT : DIR_LEFT;

            int sgn = sign(house_cx - ghost->x);
            ghost->dx = sgn;
            ghost->dy = 0;
            move(ghost, 0.5f);
            if (sgn != sign(house_cx - ghost->x)) {
                ghost->x = 14 * 8;
            }
        
        // then, move up to exit the house
        } else {
            ghost->direction = DIR_UP;
            ghost->dx = 0;
            ghost->dy = -1;
            move(ghost, 0.5f);

            if (ghost->y <= 14*8 + 4) {
                ghost->y = 14*8 + 4;
                ghost->is_leaving_house = FALSE;
                ghost_left_house(ghost, game);
            }
        }

        return;
    }

    move(ghost, 0.5f);

    // bounce up and down
    if (ghost->in_house) {
        if (ghost->dy > 0) {
            if (obj_cell_y(ghost) >= 18) {
                ghost->y = 18 * 8;
                ghost->dy = -1;
                ghost->direction = DIR_UP;
            }
        } else {
            if (obj_cell_y(ghost) <= 16) {
                ghost->y = 17 * 8;
                ghost->dy = 1;
                ghost->direction = DIR_DOWN;
            }
        }
    }
}

static void enter_house_update(Ghost* ghost) {
    // enter downwards
    if (ghost->y < 18 * 8) {
        ghost->direction = DIR_DOWN;
        ghost->dx = 0;
        ghost->dy = 1;
        move(ghost, 1.5f);
    } else {
        ghost->y = 18 * 8;

        int sgn0 = sign(ghost->ai.home_x - 14);
        int sgn = sign(ghost->ai.home_x * 8 - ghost->x);
        ghost->dy = 0;

        if (sgn0 > 0) {
            ghost->direction = DIR_RIGHT;
            ghost->dx = 1;
        } else {
            ghost->direction = DIR_LEFT;
            ghost->dx = -1;
        }

        if (sgn != sgn0) {
            // done
            ghost->x = ghost->ai.home_x * 8;
            ghost->in_house = TRUE;
            ghost->is_entering_house = FALSE;
            
            ghost->direction = DIR_UP;
            point_in_direction(ghost, DIR_UP);
        } else {
            move(ghost, 1.5f);
        }
    }
}

void ghost_left_house(Ghost* ghost, Game* game) {
    ghost->direction = DIR_LEFT;
    ghost->dx = -1;
    ghost->dy = 0;

    ghost->next_cx = obj_cell_x(ghost) + ghost->dx;
    ghost->next_cy = obj_cell_y(ghost) + ghost->dy;

    ghost->next_dir = DIR_LEFT;
    calc_next_direction(ghost, game);
}

static void point_in_direction(Ghost* ghost, int direction) {
    get_direction_vector(direction, &ghost->dx, &ghost->dy);
}

void ghost_update(Ghost* ghost, Game* game) {
    if (game->player_die) {
        animation(ghost, game);
        return;
    }

    if (ghost->in_house || ghost->is_leaving_house) {
        if (game->eaten_ghost == NULL)
            house_update(ghost, game);

        animation(ghost, game);
        return;
    }

    if (ghost->is_entering_house) {
        enter_house_update(ghost);
        animation(ghost, game);
        return;
    }

    // if a player is eating a ghost, the game will freeze
    // the ghost will not freeze if it is returning back to
    // the house after it was eaten
    if (game->eaten_ghost != NULL && (!ghost->eaten || game->eaten_ghost == ghost))
        return;

    assert(ghost->ai.base_speed != NULL);
    ghost->speed = ghost->ai.base_speed(ghost, game);

    if (ghost->fright) {
        ghost->speed = LEVEL_SPECS[game->level_index].frightened_speed;
    } else if (ghost->eaten) {
        ghost->speed = 1.5f;
    }

    // tunnel slowdown
    if (obj_cell_y(ghost) == 17 && (obj_cell_x(ghost) <= 4 || obj_cell_x(ghost) >= 23)) {
        ghost->speed = LEVEL_SPECS[game->level_index].ghost_tunnel_speed;
    }

    int last_cx = obj_cell_x(ghost);
    int last_cy = obj_cell_y(ghost);
    int last_x = ghost->x;
    move(ghost, REFERENCE_SPEED * ghost->speed);

    // if i am in new cell
    if (obj_cell_x(ghost) != last_cx || obj_cell_y(ghost) != last_cy) {
        if (ghost->reverse) {
            ghost->reverse = FALSE;
            ghost->direction = get_opposite_direction(ghost->direction);
            point_in_direction(ghost, ghost->direction);

            ghost->next_cx = obj_cell_x(ghost) + ghost->dx;
            ghost->next_cy = obj_cell_y(ghost) + ghost->dy;
        }

        calc_next_direction(ghost, game);
        ghost->direction = ghost->next_dir;
    }

    // if at center of cell that i am moving toward
    if (
        (ghost->dx != 0 && sign(ghost->x - (ghost->next_cx * 8 + 4)) == ghost->dx) ||
        (ghost->dy != 0 && sign(ghost->y - (ghost->next_cy * 8 + 4)) == ghost->dy)
    ) {
        if (ghost->dx != 0) ghost->x = ghost->next_cx * 8 + 4;
        if (ghost->dy != 0) ghost->y = ghost->next_cy * 8 + 4;
        point_in_direction(ghost, ghost->direction);

        ghost->next_cx = obj_cell_x(ghost) + ghost->dx;
        ghost->next_cy = obj_cell_y(ghost) + ghost->dy;
    }

    // ghost will enter the house
    // when it is in front of the door
    // when eaten
    if (ghost->eaten && obj_cell_y(ghost) == 14) {
        int sgn0 = sign(14 * 8 - last_x);
        int sgn1 = sign(14 * 8 - ghost->x);

        if (sgn0 != sgn1) {
            ghost->is_entering_house = TRUE;
            ghost->x = 14 * 8;
            ghost->y = 14 * 8 + 4;
        }
    }

    // screen wrapping
    if (obj_cell_x(ghost) >= 30) {
        ghost->x = -8;
        ghost->next_cx = obj_cell_x(ghost) + 1;
    }

    if (obj_cell_x(ghost) <= -3) {
        ghost->x = 28 * 8 + 4;
        ghost->next_cx = obj_cell_x(ghost) - 1;
    }

    // player collision
    if (obj_cell_x(&game->player) == obj_cell_x(ghost) && obj_cell_y(&game->player) == obj_cell_y(ghost)) {
        if (ghost->fright) {
            ghost->eaten = TRUE;
            ghost->fright = FALSE;

            game_player_ate_ghost(game, ghost);
            sound_start(&game->interface->sound, SOUND_EAT_GHOST);
        } else if (!ghost->eaten) {
            if (!game->debug) {
                game_player_ghost_touch_player(game);
            }
        }
    }

    animation(ghost, game);
}

static void calc_next_direction(Ghost* ghost, Game* game) {
    int opposite_dir = get_opposite_direction(ghost->direction);
    int x = obj_cell_x(ghost);
    int y = obj_cell_y(ghost);

    // calculate next direction
    if (ghost->fright) {
        int random_dir = game_get_random_number(game) & 0x3;

        for (int i = 0; i < 4; i++) {
            int dvec_x, dvec_y;
            get_direction_vector(random_dir, &dvec_x, &dvec_y);

            // i can turn here!
            if (random_dir != opposite_dir && !game_is_tile_solid(game, x + dvec_x, y + dvec_y)) {
                ghost->next_dir = random_dir;
                break;
            }

            // i can't turn here, look at the next direction (clockwise)
            // conveniently, direction enums are sorted clockwise
            random_dir = (random_dir + 1) % 4;
        }
    } else {
        if (ghost->eaten) {
            ghost->ai.target_x = 13;
            ghost->ai.target_y = 14;
        } else {
            assert(ghost->ai.recalculate_goal != NULL);
            ghost->ai.recalculate_goal(ghost, game);
        }

        calc_distance(ghost, game, x + 1, y    , DIR_RIGHT);
        calc_distance(ghost, game, x - 1, y    , DIR_LEFT);
        calc_distance(ghost, game, x    , y - 1, DIR_UP);
        calc_distance(ghost, game, x    , y + 1, DIR_DOWN);
        ghost->path_dists[opposite_dir] = -1.0f; // cannot go opposite direction

        // check if turning up is forbidden here
        for (int i = 0; i < sizeof(FORBIDDEN_TURNS) / sizeof(float); i += 2) {
            if (FORBIDDEN_TURNS[i] == x && FORBIDDEN_TURNS[i+1] == y) {
                ghost->path_dists[DIR_UP] = -1.0f;
                break;
            }
        }

        float min_dist = INFINITY;
        for (int i = 0; i < 4; i++) {
            if (ghost->path_dists[i] >= 0.0f && ghost->path_dists[i] <= min_dist) {
                ghost->next_dir = i;
                min_dist = ghost->path_dists[i];
            }
        }
    }
}

static void animation(Ghost* ghost, Game* game) {
    // ghost animations don't update while
    // a ghost is being eaten
    if (game->eaten_ghost) return;

    int anim_frame = (ghost->frame / 8) % 2;
    int dir_anim_offset;

    switch (ghost->direction) {
        case DIR_RIGHT:
            dir_anim_offset = 0;
            break;

        case DIR_DOWN:
            dir_anim_offset = 1;
            break;

        case DIR_LEFT:
            dir_anim_offset = 2;
            break;

        case DIR_UP:
            dir_anim_offset = 3;
            break;
    }

    if (ghost->fright) {
        ghost->sprite = SPRITE_FRIGHTENED_GHOST + anim_frame;
    } else {
        ghost->sprite = SPRITE_GHOST + dir_anim_offset * 2 + anim_frame;
    }

    ghost->frame++;
}

void ghost_draw(Ghost* ghost, Game* game, DisplayUtilState* render) {
    render->flip_x = FALSE;
    render->flip_y = FALSE;

    if (ghost->fright) {
        render->palette = game_ghost_flash(game) ? 18 : 30;
    } else if (ghost->eaten) {
        render->palette = 25;
    } else {
        render->palette = ghost->ai.palette;
    }

    display_sprite(render, ghost->x - 8, ghost->y - 8, ghost->sprite);
}

void ghost_frighten(Ghost* ghost) {
    if (ghost->eaten) return;

    ghost->fright = TRUE;
    ghost->reverse = TRUE;
}

void ghost_defrighten(Ghost* ghost) {
    ghost->fright = FALSE;
}

void ghost_set_scatter(Ghost *ghost, int value) {
    if (value != ghost->scatter)
        ghost->reverse = TRUE;
    ghost->scatter = value;
}

static int calc_distance(Ghost* ghost, Game* game, int x, int y, int index) {
    if (game_is_tile_solid(game, x, y)) {
        ghost->path_dists[index] = -1.0f;
        return FALSE;
    } else {
        int tx = ghost->ai.target_x;
        int ty = ghost->ai.target_y;

        float player_dx = (float)tx - x;
        float player_dy = (float)ty - y;
        float dist = sqrtf(player_dx * player_dx + player_dy * player_dy);

        ghost->path_dists[index] = dist;
        return TRUE;
    }
}